/*
 * The MIT License
 *
 * Copyright 2014 David Rodenas Pico.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package edu.uoc.sd.fr14.impl;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;
import edu.uoc.sd.fr14.model.CustomerId;
import edu.uoc.sd.fr14.model.FlightId;
import edu.uoc.sd.fr14.model.Nseq;
import edu.uoc.sd.fr14.model.OpId;
import edu.uoc.sd.fr14.model.ProxyId;
import edu.uoc.sd.fr14.model.ServerId;
import edu.uoc.sd.fr14.model.SubLog;
import static edu.uoc.sd.fr14.test.ServerStateCreator.*;

/**
 * Test the server implementation assuming that there is perfect network with replica.
 * No failures, fixed leader with no changes.
 * Requires update replicas state and forward to leader requests.
 *
 * @author drodenas
 */
public class ServerImplPh2 extends ServerImplCommon {
    
    public ServerImplPh2() {
    }
    
    @BeforeClass
    public static void setUpClass() {
    }
    
    @AfterClass
    public static void tearDownClass() {
    }

    @Before
    @Override
    public void setUp() {
        super.setUp();
    }
    
    @After
    @Override
    public void tearDown() {
        super.tearDown();
    }

    /* *********************************************************************
     * *
     * * CONFIRM
     * *
     * *********************************************************************/
    
    @Test
    public void confirmDirectlyFromFollower() {
        makeMeFollower();
        
        final boolean expected = false;
        final FlightId flightId = FLIGHT_FULL;
        final CustomerId customerId = CUSTOMER1000;
        ensureCallCustomerConfirmed(flightId, customerId, expected);
                
        server.confirm(flightId, customerId);
        
        assertEquals("company flight", expected, company.getFlight(flightId).confirm(customerId));
        testCalls.check();
    }

    @Test
    public void confirmDirectlyFromLeader() {
        makeMeLeader();

        final boolean expected = false;
        final FlightId flightId = FLIGHT_FULL;
        final CustomerId customerId = CUSTOMER1000;
        ensureCallCustomerConfirmed(flightId, customerId, expected);
                
        /////////////////////////////////////
        server.confirm(flightId, customerId);
        /////////////////////////////////////
        
        assertEquals("company flight", expected, company.getFlight(flightId).confirm(customerId));
        testCalls.check();
    }

    @Test
    public void confirmDirectlyFromLost() {
        makeMeLost();

        final boolean expected = false;
        final FlightId flightId = FLIGHT_FULL;
        final CustomerId customerId = CUSTOMER1000;
        ensureCallCustomerConfirmed(flightId, customerId, expected);
                
        /////////////////////////////////////
        server.confirm(flightId, customerId);
        /////////////////////////////////////
        
        assertEquals("company flight", expected, company.getFlight(flightId).confirm(customerId));
        testCalls.check();
    }

    
    /* *********************************************************************
     * *
     * * PREPARE
     * *
     * *********************************************************************/
    
    @Test
    public void prepareLeaderDoesNothing() {
        makeMeLeader();
        
        OpId op = new OpId(1);
        cluster.server(OTHER);
        server.prepare(op, OTHER);
    }

    @Test
    public void prepareFollowerRespondsPreparedToLeader() {
        makeMeFollower();        
        OpId op = new OpId(1);
        
        ensureCallLeaderPrepared(op);
        /////////////////////////////////////
        server.prepare(op, OTHER);
        /////////////////////////////////////
        testCalls.check();
    }

    @Test
    public void prepareFollowerSetLeader() {
        makeMeFollower();        
        OpId op = new OpId(1);
        ServerId leaderId = new ServerId(999);
        
        cluster.server(leaderId);
        /////////////////////////////////////
        server.prepare(op, leaderId);
        /////////////////////////////////////
        assertEquals("leader id should be who requests prepare", leaderId, serverState.getRole().getLeader());
        testCalls.check();
    }

    @Test
    public void prepareLostSetLeaderRespondsPreparedToLeader() {
        makeMeLost();        
        OpId op = new OpId(1);
        
        ensureCallLeaderPrepared(op);
        /////////////////////////////////////
        server.prepare(op, OTHER);
        /////////////////////////////////////
        testCalls.check();
    }

    @Test
    public void prepareLostSetLeader() {
        makeMeLost();        
        OpId op = new OpId(1);
        
        cluster.server(OTHER);
        /////////////////////////////////////
        server.prepare(op, OTHER);
        /////////////////////////////////////
        
        assertEquals("leader id is who request prepare", OTHER, serverState.getRole().getLeader());
        testCalls.check();
    }

    
    /* *********************************************************************
     * *
     * * PREPARE
     * *
     * *********************************************************************/
    
    public void preparedIgnores() {
        OpId opId = serverSeat(FLIGHT_EMPTY, CUSTOMER1000);
        serverCancelOps();
        
        server.prepared(opId);
        assertFalse("customer not should be added", serverState.getCompany().getFlight(FLIGHT_EMPTY).confirm(CUSTOMER1000));
        testCalls.check();
    }
    
    @Test
    public void preparedLostIgnores() {
        makeMeLost();
        preparedIgnores();
    }
    
    @Test
    public void preparedFollowerIgnores() {
        makeMeLost();
        preparedIgnores();
    }
    
    private void preparedLeader(FlightId flight, CustomerId customer) {
        preparedLeader(flight, customer, customer);
    }
    private void preparedLeader(FlightId flight, CustomerId customer, ProxyId proxyId) {
        makeMeLeader();
        // compute actions and future state
        boolean hasFreeSeats = serverState.getCompany().getFlight(flight).query() > 0;
        boolean confirmed = serverState.getCompany().getFlight(flight).confirm(customer);
        boolean apply = hasFreeSeats && !confirmed && serverState.getCompany().getFlight(flight).query() > 0;
        boolean expected = hasFreeSeats || confirmed;
        
        // prepare other state to check correct send data
        SubLog sySubLog = serverState.getLog().recover(new Nseq(0));
        otherState.getNseqTracker().advance(sySubLog, ME);
        lowerState.getNseqTracker().advance(sySubLog, ME);
        
        // ensure messages to be sent
        cluster.quorum(2);
        OpId opId = serverSeat(flight, customer, proxyId);       
        ensureCallCustomerConfirmed(flight, customer, proxyId, expected);
        if (apply) ensureCallOthersApply(flight, customer);
        
        /////////////////////////////////////
        server.prepared(opId);
        /////////////////////////////////////
        
        // check results
        assertEquals("add customer to flight", expected, serverState.getCompany().getFlight(flight).confirm(customer));
        testCalls.check();
    }
    
    @Test
    public void preparedLeaderEmptyFlight() {
        preparedLeader(FLIGHT_EMPTY, CUSTOMER1000);
    }
    
    @Test
    public void preparedLeaderFullFlight() {
        preparedLeader(FLIGHT_FULL, CUSTOMER1000);
    }
    
    @Test
    public void preparedLeaderAlmostFullFlight() {
        preparedLeader(FLIGHT_ALMOST_FULL, CUSTOMER1000);
    }
    
    @Test
    public void preparedLeaderInFlight() {
        preparedLeader(FLIGHT_CUSTOMER1000, CUSTOMER1000);
    }
    
    @Test
    public void preparedLeaderInFullFlight() {
        preparedLeader(FLIGHT_FULL_CUSTOMER1000, CUSTOMER1000);
    }
    
    @Test
    public void preparedLeaderEmptyFlightAndProxyConfirmation() {
        preparedLeader(FLIGHT_EMPTY, CUSTOMER1000, OTHER);
    }
    
    @Test
    public void preparedLeaderFullFlightAndProxyConfirmation() {
        preparedLeader(FLIGHT_FULL, CUSTOMER1000, OTHER);
    }
    
    @Test
    public void preparedLeaderAlmostFullFlightAndProxyConfirmation() {
        preparedLeader(FLIGHT_ALMOST_FULL, CUSTOMER1000, OTHER);
    }
    
    @Test
    public void preparedLeaderInFlightAndProxyConfirmation() {
        preparedLeader(FLIGHT_CUSTOMER1000, CUSTOMER1000, OTHER);
    }
    
    @Test
    public void preparedLeaderInFullFlightAndProxyConfirmation() {
        preparedLeader(FLIGHT_FULL_CUSTOMER1000, CUSTOMER1000, OTHER);
    }
    
    /* *********************************************************************
     * *
     * * APPLY
     * *
     * *********************************************************************/
    
    @Test
    public void applyExpectedNseq() {
        
        SubLog subLog = otherSubLog(FLIGHT_EMPTY, CUSTOMER1000);
        serverState.getNseqTracker().advance(subLog, OTHER);
        Nseq nseq = serverState.getNseqTracker().getCurrent(OTHER);
        
        /////////////////////////////////////
        server.apply(nseq, subLog, OTHER);
        /////////////////////////////////////
        
        assertTrue("flight should contain the customer", company.getFlight(FLIGHT_EMPTY).confirm(CUSTOMER1000));
        assertEquals("nseq for other:'"+OTHER+"'", new Nseq(2), serverState.getNseqTracker().getCurrent(OTHER));
    }
    
    @Test
    public void applyUnexpectedNseq() {
        
        SubLog subLog = otherSubLog(FLIGHT_EMPTY, CUSTOMER1000);
        Nseq nseq = new Nseq(3);
                
        cluster.server(OTHER);
        /////////////////////////////////////
        server.apply(nseq, subLog, OTHER);
        /////////////////////////////////////
        
        assertFalse("flight should not contain the customer", company.getFlight(FLIGHT_EMPTY).confirm(CUSTOMER1000));
        assertEquals("nseq for other:'"+OTHER+"'", new Nseq(0), serverState.getNseqTracker().getCurrent(OTHER));
    }

    
    /* *********************************************************************
     * *
     * * CONFIRMED
     * *
     * *********************************************************************/
    
    public void confirmedSendsValueToCustomer(FlightId flight, CustomerId customer, boolean expected) {
        
        ensureCallCustomerConfirmed(flight, customer, expected);
        
        /////////////////////////////////////
        server.confirmed(flight, customer, expected);
        /////////////////////////////////////
        
        testCalls.check();
    }

    @Test
    public void confirmedCancelsToAndSendsValueToCustomerFullTrue() {
        confirmedSendsValueToCustomer(FLIGHT_FULL, CUSTOMER1000, true);
    }

    @Test
    public void confirmedCancelsToAndSendsValueToCustomerFullFalse() {
        confirmedSendsValueToCustomer(FLIGHT_FULL, CUSTOMER1000, false);
    }

    @Test
    public void confirmedCancelsToAndSendsValueToCustomerEmptyTrue() {
        confirmedSendsValueToCustomer(FLIGHT_EMPTY, CUSTOMER1000, true);
    }

    @Test
    public void confirmedCancelsToAndSendsValueToCustomerEmptyFalse() {
        confirmedSendsValueToCustomer(FLIGHT_EMPTY, CUSTOMER1000, false);
    }

    @Test
    public void confirmedCancelsToAndSendsValueToCustomerInTrue() {
        confirmedSendsValueToCustomer(FLIGHT_CUSTOMER1000, CUSTOMER1000, true);
    }
    
    @Test
    public void confirmedCancelsToAndSendsValueToCustomerInFalse() {
        confirmedSendsValueToCustomer(FLIGHT_CUSTOMER1000, CUSTOMER1000, false);
    }
    
    /* *********************************************************************
     * *
     * * QUERY
     * *
     * *********************************************************************/
    

    @Test
    public void queryDirectlyFromLeader() {
        makeMeLeader();

        final int expected = 0;
        final FlightId flightId = FLIGHT_FULL;
        final CustomerId customerId = CUSTOMER1000;
        ensureCallCustomerQueried(flightId, customerId, expected);
        
        /////////////////////////////////////
        server.query(flightId, customerId);
        /////////////////////////////////////
        
        assertEquals("company flight", expected, company.getFlight(flightId).query());
        testCalls.check();
    }

    @Test
    public void queryDirectlyFromFollower() {
        makeMeFollower();
        
        final int expected = 0;
        final FlightId flightId = FLIGHT_FULL;
        final CustomerId customerId = CUSTOMER1000;
        ensureCallCustomerQueried(flightId, customerId, expected);
                
        /////////////////////////////////////
        server.query(flightId, customerId);
        /////////////////////////////////////
        
        assertEquals("company flight", expected, company.getFlight(flightId).query());
        testCalls.check();
    }

    @Test
    public void queryDirectlyFromLost() {
        makeMeLost();

        final int expected = 0;
        final FlightId flightId = FLIGHT_FULL;
        final CustomerId customerId = CUSTOMER1000;
        ensureCallCustomerQueried(flightId, customerId, expected);
                
        /////////////////////////////////////
        server.query(flightId, customerId);
        /////////////////////////////////////
        
        assertEquals("company flight", expected, company.getFlight(flightId).query());
        testCalls.check();
    }

    
    /* *********************************************************************
     * *
     * * SEAT
     * *
     * *********************************************************************/

    /* * SEAT FOLLOWER
     * *********************************************************************/
    
    private void seatFromFollowerExpectsLeaderSeat(FlightId flight, CustomerId customer) {
        makeMeFollower();
        
        ensureCallLeaderSeat(flight, customer);
        
        /////////////////////////////////////
        server.seat(flight, customer, customer);
        /////////////////////////////////////
        
        testCalls.check();
    }
    
    @Test
    public void seatFromFollowerExpectsLeaderSeatWhenEmpty() {
        seatFromFollowerExpectsLeaderSeat(FLIGHT_EMPTY, CUSTOMER1000);
    }
    
    @Test
    public void seatFromFollowerExpectsLeaderSeatWhenFull() {
        seatFromFollowerExpectsLeaderSeat(FLIGHT_FULL, CUSTOMER1000);
    }
    
    @Test
    public void seatFromFollowerExpectsLeaderSeatWhenIn() {
        seatFromFollowerExpectsLeaderSeat(FLIGHT_CUSTOMER1000, CUSTOMER1000);
    }

    
    /* * SEAT LOST
     * *********************************************************************/    
    
    private void seatFromLost(FlightId flight, CustomerId customer) {
        makeMeLost();
        
        /////////////////////////////////////
        server.seat(flight, customer, customer);
        /////////////////////////////////////
        
        testCalls.check();
    }
    
    @Test
    public void seatFromLostWhenEmpty() {
        seatFromLost(FLIGHT_EMPTY, CUSTOMER1000);
    }
    
    @Test
    public void seatFromLostWhenFull() {
        seatFromLost(FLIGHT_FULL, CUSTOMER1000);
    }

    @Test
    public void seatFromLostWhenIn() {
        seatFromLost(FLIGHT_CUSTOMER1000, CUSTOMER1000);
    }
    
    
    /* * SEAT LEADER
     * *********************************************************************/    

    private void seatFromLeaderExpectsPrepare(FlightId flight, CustomerId customer) {
        seatFromLeaderExpectsPrepare(flight, customer, customer);
    }
    
    private void seatFromLeaderExpectsPrepare(FlightId flight, CustomerId customer, ProxyId from) {
        makeMeLeader();
        
        boolean expect = serverState.getCompany().getFlight(flight).confirm(customer);
        ensureCallOthersPrepare(flight, customer, from);
        
        /////////////////////////////////////
        server.seat(flight, customer, from);
        /////////////////////////////////////
     
        assertEquals("flight state should not change", expect, serverState.getCompany().getFlight(flight).confirm(customer));
        testCalls.check();
    }

    @Test
    public void seatFromLeaderExpectsPrepareWhenEmpty() {
        seatFromLeaderExpectsPrepare(FLIGHT_EMPTY, CUSTOMER1000);
    }

    @Test
    public void seatFromLeaderExpectsPrepareWhenAlmostFull() {
        seatFromLeaderExpectsPrepare(FLIGHT_ALMOST_FULL, CUSTOMER1000);
    }

    @Test
    public void seatFromLeaderExpectsPrepareWhenEmptyWithProxy() {
        seatFromLeaderExpectsPrepare(FLIGHT_EMPTY, CUSTOMER1000, OTHER);
    }

    @Test
    public void seatFromLeaderExpectsPrepareWhenAlmostFullWithProxy() {
        seatFromLeaderExpectsPrepare(FLIGHT_ALMOST_FULL, CUSTOMER1000, OTHER);
    }

    
    
}
